package org.erikaredmark.monkeyshines;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.erikaredmark.monkeyshines.Conveyer.Rotation;
import org.erikaredmark.monkeyshines.resource.CoreResource;
import org.erikaredmark.monkeyshines.resource.SoundManager;
import org.erikaredmark.monkeyshines.tiles.CollapsibleTile;
import org.erikaredmark.monkeyshines.tiles.ConveyerTile;
import org.erikaredmark.monkeyshines.tiles.TileType;
import org.erikaredmark.monkeyshines.util.GameEndCallback;
import org.erikaredmark.util.collection.RingArray;
import com.google.common.base.Function;
/**
*
* Represents the main character. Object can draw itself and has awareness to the world capable of handling collision detection
*
* @author Erika Redmark
*
*/
public final class Bonzo {
// Constants for bonzo
public static final ImmutablePoint2D BONZO_SIZE = ImmutablePoint2D.of(40, 40);
public static final ImmutablePoint2D BONZO_SIZE_HALF = ImmutablePoint2D.of(20, 20);
private static final int JUMP_SPRITES = 8;
private static final int JUMP_Y = 80;
// private static final int JUMP_RIGHT_X = 0;
private static final int JUMP_LEFT_X = JUMP_SPRITES * BONZO_SIZE.x();
// Sprite Info
private int walkingDirection; // used during walking for determing what to blit.
// 1 for left, 0 for right.
private int currentSprite; // used everywhere for whatever sprite he is on for animation
/* ----------------------- location and history ------------------------ */
private int currentScreenID;
private Point2D currentLocation;
private final RingArray<LevelScreen> screenHistory;
/* ----------------------------- powerups ------------------------------ */
// Initially set to null (no powerup). When bonzo grabs powerups this will be set until a
// degrade timer unsets it after a delay
private Powerup currentPowerup;
// Internal stuff powerup decay. NO SCHEDULED THREADS because we don't want the game
// loop to use multiple threads.
private PowerupState powerupState;
/* ----------------------------- velocity ------------------------------ */
// Velocity applied to bonzo. Affected by the keyboard and falls.
private Point2D currentVelocity;
// Health goes down from long falls or certain health draining sprites
// health is bounded from 0 to GameConstants.HEALTH_MAX
private int health;
// Bonzo starts with some set amount of lives. Once that reaches zero, death is no longer
// cheap.
// If this is set to -2, bonzo has infinite lives. -1 means he's dead Jim
private int lives;
private final GameEndCallback gameEndCallback;
public static final int INFINITE_LIVES = -2;
// Score starts at zero and goes up until the game is over.
// And yes, score is a property of the character, not the world.
private int score;
private final Runnable scoreCallback;
// Use a pointer to the current world to get information such as the screen, and then from there where bonzo
// is relative to the screen.
private World worldPointer;
// Store a reference to the current resources sound manager for less indirection
// playing sounds
private SoundManager soundManager;
/* **********************************************
*
* State variables
* All states, except dying, can co-exist together. Hence, they are just booleans and not a more defined
* state machine.
*
* **********************************************/
// when :up: is hit, velocity is applied upwards. As long as jumping is yes and he has positive velocity and does NOT
// have a jetpack, slowly decrease it until it reachs the max limit (MAX_FALL_SPEED)
// Once there is ground below bonzo, stop jumping
private boolean isJumping;
// incremented every tick bonzo is not on the ground. When he lands, depending on whether he jumped
// or not, this will be used to calculate fall damage, if any.
// If bonzo is the air for so long that this overflows... well that's crazy level design.
private int timeInAir;
// When bonzo is on a conveyer, he is moved in one direction or the other based on which way the
// conveyer is rotated.
private Rotation affectedConveyer;
// When dying, everything is overridden, and bonzo is reset.
private boolean isDying;
// The current animation to use for bonzo dying. This field is only used when going through
// death animations and is only updated when bonzo is killed.
private DeathAnimation deathAnimation;
private Function<Bonzo, Void> lifeLostCallback;
/* **********************************************
*
* Animation data
* ticks to next frame means how many ticks before advancing bonzo's sprite sheet.
* this is NOT the same as ticks between updates. Updates happen every tick (collisions)
* but advancing the sprite sheet is slower.
*
* **********************************************/
private int ticksToNextFrame;
private static final int TICKS_BETWEEN_FRAMES = 0;
// Once bonzo lands, he is back in his original state, EXCEPT the animation needs to play the
// jumping frames backwards.
private boolean unJumping;
/**
*
* Creates bonzo for use in the game
*
* @param worldPointer
* reference to main world for collision detection purposes
*
* @param startingLives
* bonzo starts with this many lives. After losing all of them the game is over. Bonzo
* can only have a max of 9 lives.
*
* @param scoreCallback
* UI callback to use when bonzos score is updated (to reflect on GUI)
*
* @param gameOverCallback
* UI callback for when the game is 'over', when bonzo loses all his lives
*
* @param lifeLostCallback
* function called when bonzo loses a life, but it is NOT game over. Takes a reference
* to bonzo. NOT called if a life lost would result in a game over. If bonzo has infinite lives,
* this is still called to handle if bonzo had lost a life and had to respawn (the lives counter just
* won't be modified)
*
*/
public Bonzo(final World worldPointer,
final int startingLives,
final Runnable scoreCallback,
final GameEndCallback gameEndCallback,
final Function<Bonzo, Void> lifeLostCallback) {
this.worldPointer = worldPointer;
this.scoreCallback = scoreCallback;
this.lives = startingLives;
this.lifeLostCallback = lifeLostCallback;
this.powerupState = new PowerupState();
this.powerupState.clear();
this.gameEndCallback = gameEndCallback;
this.health = GameConstants.HEALTH_MAX;
this.soundManager = worldPointer.getResource().getSoundManager();
currentScreenID = 1000; // Always 1000. Everything starts on 1000
final LevelScreen currentScreen = worldPointer.getScreenByID(currentScreenID);
// Initialise Variables
walkingDirection = 0;
currentSprite = 0;
affectedConveyer = Rotation.NONE;
screenHistory = new RingArray<>(GameConstants.SCREEN_HISTORY);
// Initialise starting points
ImmutablePoint2D start = currentScreen.getBonzoStartingLocationPixels();
currentLocation = Point2D.from(start);
currentScreen.setBonzoCameFrom(BonzoSaveState.fromPoint(start) );
currentVelocity = Point2D.of(0, 0);
restartBonzoOnScreen(currentScreen, currentScreen.getBonzoCameFrom() );
}
/**
*
* Restarts bonzo on the given screen at the given starting location.
*
* @param screen
*
* @param startingLocation
*
*/
public void restartBonzoOnScreen(final LevelScreen screen, BonzoSaveState startingLocation) {
// The World events take care of moving Bonzo around, and Bonzo has methods to swap his position
// on screen when moving between them.
Point2D newLocation = Point2D.of(startingLocation.x, startingLocation.y);
currentLocation = newLocation;
currentVelocity.setX(startingLocation.velX);
currentVelocity.setY(startingLocation.velY);
// Not adding the current screen to history is deliberate.
currentScreenID = screen.getId();
setJumping(startingLocation.jumping);
if (startingLocation.rotation != Rotation.NONE)
{ setAffectedByConveyer(startingLocation.rotation); }
// When bonzo is restarted, he is not dead and the unjumping animation should not play
setDying(false, this.deathAnimation);
setUnjumping(false);
// Bring health back
this.health = GameConstants.HEALTH_MAX;
}
/**
*
* Changes the current screen to the new id
* <p/>
* It is the responsibility of the caller to set Bonzos new location properly
*
* @param newScreen
*/
public void changeScreen(final int newScreen) {
// keep history. We only commit to history when moving OFF a screen, so the current
// screen is not part of the history.
screenHistory.pushFront(worldPointer.getScreenByID(currentScreenID) );
this.currentScreenID = newScreen;
}
/**
*
* Calls onGround(originalPositionY) with the same original position as the current location. Used from when bonzo is
* just standing around and needs to know if there is ground beneath.
*
*/
public GroundState onGround() {
return onGround(currentLocation.y() );
}
/**
*
* returns how much health bonzo has remaining. This returns '0' if his health drops below zero.
*
* @return
*
*/
public int getHealth() {
return health > 0
? health
: 0;
}
/**
*
* Adds to bonzos health. Bonzo loses health over the course of normal gameplay; this is
* intended for special circumstances, like energy goodies. If this would otherwise
* put bonzo above {@code GameConstants.HEALTH_MAX}, bonzo is simply set to maximum.
* <p/>
* If assertions are enabled, negative values cause errors.
*
*/
public void incrementHealth(int amt) {
assert amt >= 0;
int newHealth = this.health + amt;
this.health = newHealth < GameConstants.HEALTH_MAX
? newHealth
: GameConstants.HEALTH_MAX;
}
/**
*
* Returns the number of lives Bonzo has remaining. If this is -2 that translates to 'Infinite' {@code Bonzo.INFINITE_LIVES}
*
* @return
*/
public int getLives() {
return lives;
}
/**
*
* Increments bonzos score by the specified amount. Bonzos score can never decrease in-game.
* Calling this indicates to the UI that there is a new score and that the UI should be updated.
*
* @param amt
* the amount to increase by
*
*/
public void incrementScore(int amt) {
this.score += amt;
scoreCallback.run();
}
/**
*
* Increments bonzos life count by the specified amount. Bonzo is capped at 9
* lives and will not go further.
* <p/>
* If assertions are enabled, errors will be fired if negative values are passed.
* External code is not allowed to decide when bonzo loses a life.
* <p/>
* Incrementing does not affect bonzo if he has infinite lives.
*
* @param amt
* number of lives to add. If this amount would otherwise push him over 9
* lives he is kept at 9
*
*/
public void incrementLives(int amt) {
if (this.lives == INFINITE_LIVES) return;
assert amt >= 0;
int newLives = this.lives + amt;
this.lives = newLives < 9
? newLives
: 9;
}
public int getScore() { return this.score; }
/**
*
* Returns the screen history up to {@code GameConstants.LEVEL_HISTORY}.
*
* @return
*/
public RingArray<LevelScreen> getScreenHistory() {
return screenHistory;
}
/**
*
* Called upon collision with the exit door. This effectively is part of the
* control flow that ends the game.
*
*/
void hitExitDoor() {
gameEndCallback.gameOverWin(worldPointer);
}
/**
* Determines if bonzo has hit the ground. Intended ONLY to be called if bonzo is currently in a jump state. If he
* hits the ground, speed considerations may make it possible for him to go through the ground a couple units. The returned
* value indicates how far to 'bump' bonzo up if he goes through the ground too far. This should only be called once
* per tick and result used and/or stored. It does modify tile state in some cases.
* <p/>
* <strong> The speed bonzo is falling must NOT exceed one minus the verticle size of the tile!</strong> Otherwise
* he will end up being bumped up to the next tile down, being inside a solid. Terminal velocity should never reach above
* that amount.
* <p/>
* This method does not modify any major state OF BONZO and merely returns a value. The value contains how far bonzo must be
* pushed up, a rotation amount if bonzo is on a conveyer, and any collapsable tiles that may need collapsing if
* bonzo is on the ground properly. This method modifies ONE minor state variable; the fall assist
* <p/>
* The parameters for original position are required to determine, for dealing with thru blocks, if bonzo fell onto
* the block, or if he was already inside of it.
*
* @param originalPositionY
* bonzo's original y position before changing. Used to determine if he fell on a block. For thrus, this means
* preventing him from snapping on top if he just missed it and is now inside it.
*
* @return
* Ground state object; primary value is the snapUpBy value:
* {@code -1} if not on the ground, other a positive value indicating how deep into the ground bonzo is. This may
* return 0... in which case bonzo is perfectly fine on the ground.
*
*/
public GroundState onGround(int originalPositionY) {
// If rising, not falling, no need to check for ground. In fact, we are allowed to go through certain ground.
if (currentVelocity.precisionY() < 0) return GroundState.RISING;
LevelScreen currentScreen = worldPointer.getCurrentScreen();
int bonzoOneBelowFeetY = (currentLocation.y() + BONZO_SIZE.y() ) + 1;
// Four points, each point 'snaps' to a tile. We need to check two centres, otherwise it is possible for bonzo
// to be flanked by emptiness, be right in the middle of a solid block, and fall through.
TileType[] grounds = new TileType[4];
int bonzoSizeXHalf = BONZO_SIZE_HALF.x();
final TileMap map = currentScreen.getMap();
grounds[0] = map.getTileXYPixel(currentLocation.x() + GameConstants.FALL_SIZE, bonzoOneBelowFeetY);
grounds[1] = map.getTileXYPixel(currentLocation.x() + bonzoSizeXHalf, bonzoOneBelowFeetY);
grounds[2] = map.getTileXYPixel(currentLocation.x() + bonzoSizeXHalf + 1, bonzoOneBelowFeetY);
grounds[3] = map.getTileXYPixel(currentLocation.x() + (BONZO_SIZE.x() - 1 - GameConstants.FALL_SIZE), bonzoOneBelowFeetY );
// State variables for later in the method. We want to loop over the tile types returned only one time.
Rotation onConveyer = Rotation.NONE;
boolean atLeastThru = false;
boolean atLeastGround = false;
// Because we are looking at four positions, with a max of three unique tiles and possibly two,
// there may be repeats. We can't use == to check due to stateless tile types but the repeats
// are required checking for any code modifying tile state.
TileType pastTile = null;
List<CollapsibleTile> mayCollapse = new ArrayList<>(4);
for (TileType t : grounds) {
if (t instanceof ConveyerTile) {
if (onConveyer == Rotation.NONE) onConveyer = ((ConveyerTile) t).getConveyer().getRotation();
else {
// Are the rotations the same? If not, choose one to take precedence according to the
// following order
// 1) Bonzo's current conveyer rotation state variable IF NOT NONE
// 2) The first conveyer selected (which would be the leftmost one)
Rotation newRotation = ((ConveyerTile) t).getConveyer().getRotation();
if (onConveyer != newRotation) {
if (this.affectedConveyer != Rotation.NONE) onConveyer = this.affectedConveyer;
// else don't change the current conveyer.
}
}
}
if (t.isThru() ) {
atLeastThru = true;
atLeastGround = true;
}
if (t.isSolid() ) atLeastGround = true;
// If on a collapsing tile, save it. It will be returned as part of
// the ground state for collapsing (should only do so if bonzo is on the tile
if (t != pastTile && t instanceof CollapsibleTile) {
mayCollapse.add((CollapsibleTile)t);
}
pastTile = t;
}
// Check #2; bonzo may be INSIDE the ground. Determine if he is and how much to snap him up by.
// If at least part of him is on a thru tile.
// Thrus differ from solids; he could jump up into a thru. We must handle that case. Solids are more simple.
if (atLeastThru) {
// If bonzo is already exactly on the ground, everything is fine. Otherwise, we may have to fall through it.
int depth = (bonzoOneBelowFeetY - 1) % GameConstants.TILE_SIZE_Y;
if (depth == 0) return new GroundState(0, onConveyer, mayCollapse);
// Very important! If we are inside of a thru, we do NOT bounce onto it unless bonzo's original position was
// ABOVE the thru. Otherwise, it is too easy for him to snap up if a jump didn't quite make it.
// This takes bonzos original location, and compares it to the current location. If he came from above they will
// snap to different tiles. We look at the TOP of bonzo, but because he is 40x40 and divides evenly into tile
// sizes, if his top points snap to different tiles, so would his bottom points.
// One exception. If bonzos original location was RIGHT ON the tile border at the y point, it still counts as a
// landing (this is for hitting a ceiling with a thru right below), thus the - 1 fudge factour
if ( (originalPositionY - 1) / GameConstants.TILE_SIZE_Y == currentLocation.y() / GameConstants.TILE_SIZE_Y) {
return new GroundState(-1, onConveyer, mayCollapse);
// Effectively, if we snap the original position and the current position and we end up at the same tile, then
// we approached it from the side, not above.
}
// Done. Thrus always toggle 'at least ground' which will run next if statement for calculating 'bounce up'
// effect. This if statement was just to prevent bounce up if he shouldn't bounce up.
}
if (atLeastGround) {
//We need to make sure that we are exactly on the thing, we don't budge it.
// bonzoOneBelowFeet - 1 gives us bottom position of bonzo. Special case for when this
// variable is aligned % = 0, it means bonzo is already on the ground. Return 0 for those
// instances to prevent snapping up a full tile.
if (bonzoOneBelowFeetY % GameConstants.TILE_SIZE_Y == 0) return new GroundState(0, onConveyer, mayCollapse);
else {
return new GroundState( (bonzoOneBelowFeetY - 1) % GameConstants.TILE_SIZE_Y, onConveyer, mayCollapse);
}
}
return new GroundState(-1, onConveyer, mayCollapse);
}
/**
*
* C-style struct for returning multiple values from the onGround method.
*
* @author Erika Redmark
*
*/
private static final class GroundState {
public final int snapUpBy;
public final Rotation onConveyer;
public final List<CollapsibleTile> mayCollapse;
private GroundState(final int snapUpBy, final Rotation onConveyer, final List<CollapsibleTile> mayCollapse) {
this.snapUpBy = snapUpBy;
this.onConveyer = onConveyer;
this.mayCollapse = Collections.unmodifiableList(mayCollapse);
}
// Immutable singleton for when bonzo is rising, not falling.
private static final GroundState RISING = new GroundState(-1, Rotation.NONE, Collections.<CollapsibleTile>emptyList() );
}
/**
*
* Checks if there is a solid block at the given x co-ordinate, that would interfere with bonzo's movement if he was
* to try to move there
*
* @param newX
*
* @return
* {@code true} if there is a solid block in that position, {@code false} if otherwise
*
*/
public boolean solidToSide(final int newX) {
// We give a little 'lee way', we don't check the very top or bottom, but a little off the extremes.
// This allows Bonzo to fit easily into 2 space open passageways and then the ground snap algorithms
// can take effect.
final TileMap map = worldPointer.getCurrentScreen().getMap();
if ( map.getTileXYPixel(newX, currentLocation.y() + 4 ).isSolid()
|| map.getTileXYPixel(newX, currentLocation.y() + BONZO_SIZE.y() - 1 - 4).isSolid()
|| map.getTileXYPixel(newX, currentLocation.y() + BONZO_SIZE_HALF.y() ).isSolid() ) {
return true;
}
return false;
}
/**
*
* Checks if there is a solid block at the given y co-ordinate, that would interfere with bonzo's jumping if he
* was to try to jump. X locations taken from state.
* <p/>
* This method has a special case: If bonzo hits a solid on either the left or right BUT he is a few pixels
* off from jumping through an opening (at least two air tiles), this will automatically correct his X position
* and return that no solid is up. This is done ONLY when he is jumping and ONLY when he is only a few pixels off.
*
* @param newY
* the 'new' y level bonzo is or is going to be
*
* @return
* {@code true} if there is a solid block in that position, {@code false} if otherwise
*
*/
public boolean solidToUp(final int newY) {
LevelScreen currentScreen = worldPointer.getCurrentScreen();
TileType[] above = new TileType[6];
// The two 'early' middle points will never refer to the same tile, but may refer to different tiles
// from extreme edge.
// Two early middles being Open but others showing a solid will activate the special case, snapping
// bonzo in place.
// Two interior middles are to make sure bonzo can't jump through a single solid block above him.
// 0,5 = extremes
// 1,4 = exterior 'middles' intended for snapping special case
// 2,3 == truly middle, middles, intended to make sure there is no single solid block
// hiding.
final TileMap map = currentScreen.getMap();
above[0] = map.getTileXYPixel(currentLocation.x(), newY);
above[1] = map.getTileXYPixel(currentLocation.x() + 2, newY );
above[2] = map.getTileXYPixel(currentLocation.x() + BONZO_SIZE_HALF.x(), newY ); // prefers left tile snap
above[3] = map.getTileXYPixel(currentLocation.x() + BONZO_SIZE_HALF.x() + 1, newY ); // prefers right tile snap
above[4] = map.getTileXYPixel(currentLocation.x() + (BONZO_SIZE.x() - 1) - 2, newY );
above[5] = map.getTileXYPixel(currentLocation.x() + (BONZO_SIZE.x() - 1), newY );
boolean atLeastSolid = false;
for (TileType t : above) {
if (t.isSolid()) atLeastSolid = true;
}
// No solids no problem
if (!(atLeastSolid) ) return false;
// Solids? Check our special case. If we can't use that then it is a solid wall.
if ( !(above[1].isSolid() )
&& !(above[2].isSolid() )
&& !(above[3].isSolid() )
&& !(above[4].isSolid() ) ) {
// Activate special case: Snap bonzo to nearest tile boundary, which should
// be enough to line him up to move up.
int bonzoNormalised = currentLocation.x() / GameConstants.TILE_SIZE_X;
int rounding = currentLocation.x() % GameConstants.TILE_SIZE_X;
// Techincally, he can ONLY be within about 2 pixels from the side, but to be safe
// we just use the middle (tile size half) to determine whether to round up the
// normalised position or keep it truncated.
if (rounding > GameConstants.TILE_SIZE_X_HALF) ++bonzoNormalised;
// Apply change to location. remember the normalised position is a tile position!
currentLocation.setX(bonzoNormalised * GameConstants.TILE_SIZE_X);
// Nothing to above now!
return false;
} else {
return true;
}
}
/**
*
* Hurts bonzo by the given amount, draining his health. If it drops below zero during this call, bonzo
* is killed. Note that invincibility will be ignored; being hurt whilst invincible implies fall damage,
* which invincibility should not be protecting from.
* <p/>
* The thing hurting bonzo must pass in a death animation enum indicating what death type should be used
* if bonzo was to die from being hurt. Damage from fall is eliminated when in possesion of a wing
* powerup.
*
* @param amt
* amount to hurt bonzos health
*
* @param animation
* if bonzo does die, this is the animation that should be used
*
*/
public void hurt(int amt, DamageEffect effect) {
if (effect == DamageEffect.FALL && (currentPowerup != null && currentPowerup.isWing() ) ) return;
if (effect == DamageEffect.BEE && (currentPowerup != null && currentPowerup.isShield() ) ) return;
health -= amt;
soundManager.playOnce(effect.soundEffect);
if (health < 0) kill(effect.deathAnimation);
}
/**
*
* Tries to kill bonzo. This takes into account if bonzo if invincible: if he is, does
* nothing. Otherwise, kills bonzo.
* <p/>
* this should not be used with the wing powerup, as falling from a high height and losing
* health should always kill bonzo. Wing powerup needs to simply prevent fall damage.
* <p/>
* If bonzo would otherwise explode, this also plays the explosion sound but still does not
* kill him
*
* @param
* if bonzo does die, use this death animation
*
*/
public void tryKill(DeathAnimation animation) {
if (currentPowerup == null || !(currentPowerup.isShield() ) ) {
kill(animation);
}
}
/**
*
* Kills bonzo. He stops moving and begins the given death animation. After effects of death (such
* as level reset) are deferred until the animation finishes.
*
* @param
* uses this death animatino for bonzo
*
*/
public void kill(DeathAnimation animation) {
currentVelocity.setX(0);
currentVelocity.setY(0);
currentSprite = 0;
timeInAir = 0;
currentPowerup = null;
powerupState.clear();
setDying(true, animation);
soundManager.playOnce(animation.soundEffect() );
}
/**
*
* Moves bonzo at the given velocity, augmented by GameConstants. Given velocity is normally
* a simple +/-1 multiplier to determine direction.
*
* @param velocity
* velocity to move Bonzo
*
*/
public void move(double velocity) {
// no zombies
if (isDying) return;
// If we are not jumping, unjumping, or dying, increment the sprite
// basically, as long as no other state is controlling animation, animate.
if ( !(isJumping)
&& !(isDying)
&& !(unJumping) ) {
if (readyToAnimate() ) {
currentSprite++;
if (currentSprite >= 16)
currentSprite = 0;
}
}
// Check if there is a solid. If not, walk there. Otherwise, we need to snap
// him against the solid denying movement but allowing him to hug it. Otherwise
// it will be impossible for the player to get aligned for jumping
double newX = currentLocation.precisionX() + ( velocity * GameConstants.BONZO_SPEED_MULTIPLIER );
this.walkingDirection = velocity < 0 ? 1 : 0; // 1 for left, 0 for right.
int solidCheck = this.walkingDirection == 1
? (int)newX
: ((int)newX) + Bonzo.BONZO_SIZE.x();
if (!solidToSide(solidCheck) ) currentLocation.setX(newX);
else snapBonzoX();
// Once bonzo lands, he only has a few moves where he can be close to the edge. this doesn't prevent him from
// falling, just allows him to get a few pixels closer to the edge.
// if (fallAssistGrace > 0) --fallAssistGrace;
}
/**
*
* Snaps bonzo's X position to be aligned with the tiles, such that % the size of a tile
* is zero. This is intended for hitting walls. See warning for bonzo speed in GameConstants.
* <p/>
* The snapping is done via whatever tile column he is closest to.
*
*/
private void snapBonzoX() {
// We divide by tile size X later to 'snap'. We add one to that result if bonzo was
// closer to the OTHER side (his remainder was greater than halfway there)
int rounding = this.currentLocation.x() % GameConstants.TILE_SIZE_X < 10
? 0
: GameConstants.TILE_SIZE_X;
int locationNormalised = (this.currentLocation.x() / GameConstants.TILE_SIZE_X) * GameConstants.TILE_SIZE_X;
this.currentLocation.setX(locationNormalised + rounding);
}
/**
*
* Snaps bonzo's Y position to be aligned with the tiles. This is typically used when bonzo
* hits the ceiling to prevent him from going slightly 'thru' the ceiling, which completely
* ruins the snapping algorithms that the standard 'move' method uses.
*
*/
private void snapBonzoY() {
int rounding = this.currentLocation.y() % GameConstants.TILE_SIZE_Y < 10
? 0
: GameConstants.TILE_SIZE_Y;
int locationNormalised = (this.currentLocation.y() / GameConstants.TILE_SIZE_Y) * GameConstants.TILE_SIZE_Y;
this.currentLocation.setY(locationNormalised + rounding);
}
/**
*
* Jump action from player input: If there is no solid directly above Bonzo, then this will set bonzo's state to jumping
* and apply some starting y velocity.
*
* @param velocity
* starting velocity to apply
*
*/
public void jump(double velocity) {
// Don't start a jump if there isn't enough room.
if (solidToUp(currentLocation.y() - 1) ) return;
// Only allow jumping if on the ground and velocity going up is zero. And not already in the middle of a jump
if (isJumping) return;
// Conveyer state at this point is irrelevant. Update method handles that.
GroundState groundState = onGround();
if (groundState.snapUpBy != -1 && (currentVelocity.y() == 0) ) {
currentVelocity.setY(-(velocity * GameConstants.BONZO_JUMP_MULTIPLIER) );
setJumping(true);
currentSprite = 0;
}
}
/**
*
* Makes bonzo stop moving in terms of velocity. If bonzo is under the effects of a conveyer
* belt those effects will be removed.
*
*/
public void stopMoving() {
currentVelocity.setX(0);
setAffectedByConveyer(Rotation.NONE);
}
/**
*
* Sets bonzo to the 'dying' state. This just plays whatever animation was requested, and
* after the animation is finished post-die animatino routines are run (like level reset)
*
* @param dying
* {@code true} to set to dying, {@code false} to stop dying (used after level reset)
*
* @param animation
* animation to use for death. Ignored if setting the dying to false
*
*/
private void setDying(boolean dying, DeathAnimation animation) {
this.isDying = dying;
this.deathAnimation = animation;
}
/**
*
* Sets bonzos state to jumping. This only affects unjump state by disabling it if
* it was on already. It does not do the reverse (auto toggling it).
*
* @param jumping
*
*/
private void setJumping(boolean jumping) {
this.isJumping = jumping;
if (unJumping) setUnjumping(false);
// Jumping from ground toggles the safe bonzo ground state for the screen
if (jumping) {
worldPointer.getScreenByID(currentScreenID).setBonzoLastOnGround(getCurrentLocation() );
}
}
private void setUnjumping(boolean unjump) {
this.unJumping = unjump;
if (unJumping) {
// Sprite 6 is the last jump sprite, so the 'first' for unjumping
this.currentSprite = 6;
} else {
// This is the first normal move sprite bonzo will be set to
// This differs when moving left vs right due to how the sprite sheet is designed.
// When moving right, as he falls his arms need to swing back, not towards the front.
if (walkingDirection == 0) this.currentSprite = 6; // right
else this.currentSprite = 0; // left
}
}
/**
*
* Indicates that bonzo has collected a powerup. This is the entry point for giving Bonzo all
* powerups; class internally handles scheduling time to expire rules and overriding current powerups
* if any exist.
*
* @param powerup
* the poweup bonzo collected. May not be {@code null}
*
*/
public void powerupCollected(Powerup powerup) {
assert powerup != null : "Cannot collect a null powerup!";
currentPowerup = powerup;
powerupState = new PowerupState();
}
/**
*
* Returns bonzos current powerup, or {@code null} if bonzo does not have one
*
* @return
* current powerup or {@code null}
*
*/
public Powerup getCurrentPowerup() {
return currentPowerup;
}
/**
*
* Returns if the powerup is visible in the UI. A powerup is not visible if either
* 1) Bonzo has no powerup
* 2) Bonzo's powerup is fading and it is in a 'flash off' phase.
*
* @return
* {@code true} if a powerup exists that should be drawn, {@code false} otherwise.
*
*/
public boolean powerupUIVisible() {
if (currentPowerup == null) return false;
// Because of the way the flashCount is implemented in the state, odd numbers (which is what
// it starts with) represents a visible powerup, where as even numbers including zero represent
// show nothing.
return powerupState.flashCount % 2 == 1;
}
/**
*
* Sets if bonzo is under the effects of a conveyer belt. Just as in the original game,
* this can overlap with jumping.
* <p/>
* Rotation indicates which direction bonzo moves. Rotation can be gathered from the conveyer tile
* during collision detection.
*
* @param affectedConveyer
* The rotation of the conveyer belt affecting bonzo, or {@code Rotation.NONE} for no effects
*
*/
private void setAffectedByConveyer(Rotation affectedConveyer) {
assert affectedConveyer != null;
this.affectedConveyer = affectedConveyer;
}
// We don't increment the sprite unless we are jumping or dying.
public void update() {
// If we are dying, animate the sprite and do nothing else
if (isDying) {
if (readyToAnimate() ) {
currentSprite++;
if (currentSprite >= 15) {
// Death animation over. If lives remain, restart bonzo. Otherwise UI callback
if (lives != INFINITE_LIVES) {
--lives;
}
// Equality because -2 is infinite
assert lives >= -2 : "Lives can be -2 (infinite) -1 (dead) and 0-9, but never less than -2";
if (lives == -1) {
gameEndCallback.gameOverFail(worldPointer);
} else {
lifeLostCallback.apply(this);
}
}
return;
}
}
int originalY = currentLocation.y();
currentLocation.applyVelocity(currentVelocity);
// At this time, we could be inside of a block. Since by game nature velocity is only either
// upward or downward, check now to bump us out of the ground
GroundState groundState = onGround(originalY);
// Set to true if bonzo was jumping or falling previously before hitting the ground.
boolean landed = false;
/* ------- Not on the ground, so start pulling downward ---------- */
// Conveyer state is MAINTAINED.
if ( groundState.snapUpBy == -1) {
++timeInAir;
// if the current velocity has not yet hit terminal
if (currentVelocity.precisionY() <= GameConstants.TERMINAL_VELOCITY ) {
if (isJumping) currentVelocity.translateYFine(GameConstants.BONZO_FALL_ACCELERATION_JUMP);
else currentVelocity.translateYFine(GameConstants.BONZO_FALL_ACCELERATION_NORMAL);
}
/* --------- On the ground. Keep y Velocities at zero ---------- */
// Conveyer state is modified to whatever the ground state says it should.
} else {
currentVelocity.setY(0);
currentLocation.translateY(-groundState.snapUpBy); //Push back to level field.
// If we are jumping when we land, start the 'unjump' animation. Also, set
// the 'threshold' for falling time for fall calculations
int fallThreshold;
if (isJumping) {
setJumping(false);
setUnjumping(true);
fallThreshold = GameConstants.SAFE_FALL_JUMP_TIME;
// Landing on ground makes this a safe respawn if required ONLY if it doesn't kill bonzo.
// We set a boolean that will toggle him being on the ground only if after health calculations
// he is still alive.
landed = true;
} else {
fallThreshold = GameConstants.SAFE_FALL_TIME;
}
// Apply fall damage if bonzo was beyond the threshold.
int airDifference = timeInAir - fallThreshold;
//System.out.println(timeInAir + " - " + fallThreshold + " = " + airDifference);
//System.out.println(airDifference);
if (airDifference > 0) {
// Do not fear the casts: Cast airDifference to double to apply multiplier, then back to int to
// get discrete units of damage to apply.
hurt((int)( (Math.pow( ((double)airDifference), GameConstants.FALL_DAMAGE_MULTIPLIER) ) ), DamageEffect.FALL);
}
// If he is still alive, go ahead and set ground state
if (landed && !(isDying) ) {
worldPointer.getScreenByID(currentScreenID).setBonzoLastOnGround(getCurrentLocation() );
}
// Whether fall damage or not, bonzo no longer in air.
timeInAir = 0;
// Set conveyer belt state
setAffectedByConveyer(groundState.onConveyer);
// We landed, so any collapsibles must collapse.
for (CollapsibleTile c : groundState.mayCollapse) {
c.collapse();
}
}
/* ------------------------ Collision Checking --------------------------- */
// Give all collisions to World
worldPointer.checkCollisions(this);
/* ---------------------------- Jump Logic ------------------------------- */
// if we are jumping, slowly increment the sprite until we get to the end, then leave it there.
if (isJumping) {
// check for a tile above us
if (!solidToUp(currentLocation.y() ) ) {
if (currentSprite < 7) ++currentSprite;
} else {
// We did hit a solid
// Only reverse the velocity if we are actually already going up. If we are falling and somehow still
// inside a solid, don't reverse again or we get bouncing through ceilings.
if (currentVelocity.y() < 0) {
currentVelocity.reverseY();
snapBonzoY();
}
}
}
// If landing from a jump, we are in an unjumping state; sole goal is to play sprites backwards.
if (unJumping) {
// Technically, sprite 0 is the end, but if we go up to it, we have 1 frame of bad animation.
// Sprite 0 will be set with unjumping becoming false.
if (currentSprite > 0) --currentSprite;
else setUnjumping(false);
}
/* ----------------------------- Conveyers ------------------------------ */
// If we are affected by a conveyer, we are moved. If we are moved into a wall, we move just enough
// to touch it.
double conveyerMovement = this.affectedConveyer.translationX();
// required to have one loop handle two directions.
double conveyerMultiplier = conveyerMovement < 0.0
? -1.0
: 1.0;
// Find farthest point not in a solid block that we can move to. Since double values
// truncate to integer for actual position, we just look at truncated ints.
// Loop does not execute if movement is 0
// It is perfectly possible for no additional movement to be applied if against a wall.
for (double i = Math.abs(conveyerMovement); i >= 0; i--) {
double translation = i * conveyerMultiplier;
double newX = this.currentLocation.precisionX() + translation;
// Truncated value will be Bonzo's 'effective' location on the world at the given time.
if (!(solidToSide((int)newX) ) ) {
currentLocation.setX(newX);
break;
}
}
/* ------------------------------ Powerup Decay ------------------------------ */
powerupState.update();
updateReadyToAnimate();
}
/**
*
* Determines if bonzo's sprite should be updated, or if it must wait another tick.
* Calling this method does NOT update the tick count, unlike other objects that
* use a similiar trick. ticksToNextFrame is updated in the update method.
*
* @return
*/
private boolean readyToAnimate() {
return this.ticksToNextFrame >= TICKS_BETWEEN_FRAMES;
}
private void updateReadyToAnimate() {
if (ticksToNextFrame >= TICKS_BETWEEN_FRAMES) ticksToNextFrame = 0;
else ++ticksToNextFrame;
}
public void paint(Graphics2D g2d) {
// If dying, that overrides everything.
if (isDying) {
ImmutablePoint2D deathStart = deathAnimation.deathStart();
ImmutablePoint2D deathSize = deathAnimation.deathSize();
ImmutablePoint2D offset = deathAnimation.offset();
int drawToX = currentLocation.x() + offset.x();
int drawToY = currentLocation.y() + offset.y();
int yOffset = deathStart.y() + (deathSize.y() * (currentSprite / deathAnimation.framesPerRow() ) );
int xOffset = deathSize.x() * (currentSprite % deathAnimation.framesPerRow() );
g2d.drawImage(CoreResource.INSTANCE.getBonzoSheet(), drawToX, drawToY, //DEST
drawToX + deathSize.x(), drawToY + deathSize.y(), // DEST2
xOffset, yOffset, xOffset + deathSize.x(), yOffset + deathSize.y(),
null);
return;
} else {
// We can just get the draw location and assume 40x40
ImmutablePoint2D sourceLocation = getDrawLocationInSprite();
g2d.drawImage(CoreResource.INSTANCE.getBonzoSheet(),
currentLocation.x(), currentLocation.y(),
currentLocation.x() + BONZO_SIZE.x(), currentLocation.y() + BONZO_SIZE.y(),
sourceLocation.x(), sourceLocation.y(),
sourceLocation.x() + BONZO_SIZE.x(), sourceLocation.y() + BONZO_SIZE.y(),
null);
}
}
/**
*
* Returns a point representing where bonzo is currently on the screen. The returned point is immutable and
* represents a snapshot of where he was when the method was called.
*
* @return
* immutable point indicating where bonzo is on the screen at the time of the call
*
*/
public ImmutablePoint2D getCurrentLocation() {
return ImmutablePoint2D.from(currentLocation);
}
/**
* Returns a point representing bonzos current velocity. The returned point is immutable and
* represents a snapshot of his velocity when the method was called
* @return
*/
public ImmutablePoint2D getCurrentVelocity() {
return ImmutablePoint2D.from(currentVelocity);
}
/** Returns if bonzo is currently in a jump state. */
public boolean isJumping() { return isJumping; }
/** Returns any current conveyer effects bonzo is under */
public Rotation getCurrentConveyerEffect() { return affectedConveyer; }
/**
*
* Returns the actual mutable point representing Bonzo's position. This method should be used with care in the smallest
* possible scope. Clients should never hold a reference to the returned point.
* <p/>
* This reference is <strong> not </strong> guaranteed to always remain valid with respect to Bonzo.
*
* @return
* mutable location
*
*/
public Point2D getMutableCurrentLocation() {
return this.currentLocation;
}
/**
*
* Explicitly sets bonzos current location on the screen based on the immutable point
*
* @param location
*
*/
public void setCurrentLocation(ImmutablePoint2D location) {
this.currentLocation = Point2D.from(location);
}
/**
*
* Returns the region that bonzo currently occupies on the screen
*
* @return
* immutable rectangle for the occupied region
*
*/
public ImmutableRectangle getCurrentBounds() {
return ImmutableRectangle.of(this.currentLocation.x(), this.currentLocation.y(), BONZO_SIZE.x(), BONZO_SIZE.y());
}
/**
*
* Returns the exact point in the sprite sheet bonzo's current frame of animation is.
* <p/>
* It is an error to call this method whilst bonzo is dying, as the sprite frame is no longer guaranteed
* to be 40x40, and bonzo shouldn't be colliding with anything after already having been colliding with
* thing already.
*
* @return
* location in sprite sheet that is currently the source frame of bonzos animation at the time
* of this call
*
*/
public ImmutablePoint2D getDrawLocationInSprite() {
if (isDying) throw new IllegalStateException("Can't get 40x40 draw location during a death animation");
// if walking right
int takeFromX = currentSprite * BONZO_SIZE.x();
// Standard Drawing
if (!(isJumping) && (!unJumping) ) {
return ImmutablePoint2D.of(takeFromX, walkingDirection * 40);
// Jump/Unjump drawing
} else {
// if we are jumping to the left, we have to go 8 * 40 to the right to get to the right sprite level
if (walkingDirection == 1) takeFromX += JUMP_LEFT_X;
return ImmutablePoint2D.of(takeFromX, JUMP_Y);
}
}
/**
*
* Encapsulates the state of a powerup. When a powerup is collected, it goes through several states. First
* is normal; it simply is there and has an effect. After a given number of ticks, it transitions into
* warning state, where it flashes visible and invisible for a given number of ticks. Finally the powerup
* is removed from bonzo.
* <p/>
* If at any time a new powerup is collected, the current state object will be destroyed and replaced with
* a new one. This effectively means bonzo may only carry one powerup at a time.
*
* @author Erika Redmark
*
*/
private final class PowerupState {
// Convert game speed (in game constants) to number of ticks to apporximate the speed
private static final int START_TICKS = GameConstants.MAX_POWERUP_TIME / GameConstants.GAME_SPEED;
private static final int FLASH_TICKS = GameConstants.TIME_BETWEEN_FLASHES / GameConstants.GAME_SPEED;
// + 1 is for the initial decrement of flashCount when ticksForDegrade first hits zero.
private static final int TOTAL_FLASHES = (GameConstants.MAX_WARNINGS * 2) + 1;
// Set to a value and decremented until zero when a powerup is active.
private int ticksForDegrade;
private int flashCount;
private boolean done;
// Creates a new powerup state instance. Since powerup information is stored in bonzo, this only
// initialises timing relevant data.
private PowerupState() {
// Ticks to degrade is re-used after hitting zero; it resets itself
// and decrements flash count. Each decrement changes a special state in
// bonzo that changes whether the powerup should be drawn even if it is still
// active. When both hit zero, the state is 'finished'
// Note: we are converting
ticksForDegrade = START_TICKS;
flashCount = TOTAL_FLASHES;
done = false;
}
// Updates the state. If the state reaches final state (powerup completely faded)
// has the side effect of setting bonzos current powerup to null
private void update() {
if (done) return;
if (ticksForDegrade <= 0) {
if (flashCount <= 0) {
done = true;
currentPowerup = null;
return;
}
--flashCount;
ticksForDegrade = FLASH_TICKS;
// Play warning sound for each even flash count
if (flashCount % 2 == 0) soundManager.playOnce(GameSoundEffect.POWERUP_FADE);
}
--ticksForDegrade;
}
// Puts this state into the end state promptly. Only used by constructor for initial state.
private void clear() {
ticksForDegrade = 0;
flashCount = 0;
done = true;
}
}
// Intialises immutable singleton to be used as inital state (when there is no powerup)
// so calling update does nothing. After bonzo collects his first powerup, this won't be
// used anymore (all powerup states naturally decay to this state)
}